a tool for shared writing and social publishing
1"use client";
2import { publishToPublication } from "actions/publishToPublication";
3import { DotLoader } from "components/utils/DotLoader";
4import { useState, useRef } from "react";
5import { ButtonPrimary } from "components/Buttons";
6import { Radio } from "components/Checkbox";
7import { useParams } from "next/navigation";
8import Link from "next/link";
9import { AutosizeTextarea } from "components/utils/AutosizeTextarea";
10import { PubLeafletPublication } from "lexicons/api";
11import { publishPostToBsky } from "./publishBskyPost";
12import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
13import { AtUri } from "@atproto/syntax";
14import { PublishIllustration } from "./PublishIllustration/PublishIllustration";
15import { useReplicache } from "src/replicache";
16import {
17 BlueskyPostEditorProsemirror,
18 editorStateToFacetedText,
19} from "./BskyPostEditorProsemirror";
20import { EditorState } from "prosemirror-state";
21
22type Props = {
23 title: string;
24 leaflet_id: string;
25 root_entity: string;
26 profile: ProfileViewDetailed;
27 description: string;
28 publication_uri: string;
29 record?: PubLeafletPublication.Record;
30 posts_in_pub?: number;
31};
32
33export function PublishPost(props: Props) {
34 let [publishState, setPublishState] = useState<
35 { state: "default" } | { state: "success"; post_url: string }
36 >({ state: "default" });
37 return (
38 <div className="publishPage w-screen h-full bg-bg-page flex sm:pt-0 pt-4 sm:place-items-center justify-center">
39 {publishState.state === "default" ? (
40 <PublishPostForm setPublishState={setPublishState} {...props} />
41 ) : (
42 <PublishPostSuccess
43 record={props.record}
44 publication_uri={props.publication_uri}
45 post_url={publishState.post_url}
46 posts_in_pub={(props.posts_in_pub || 0) + 1}
47 />
48 )}
49 </div>
50 );
51}
52
53const PublishPostForm = (
54 props: {
55 setPublishState: (s: { state: "success"; post_url: string }) => void;
56 } & Props,
57) => {
58 let [shareOption, setShareOption] = useState<"bluesky" | "quiet">("bluesky");
59 let editorStateRef = useRef<EditorState | null>(null);
60 let [isLoading, setIsLoading] = useState(false);
61 let [charCount, setCharCount] = useState(0);
62 let params = useParams();
63 let { rep } = useReplicache();
64
65 async function submit() {
66 if (isLoading) return;
67 setIsLoading(true);
68 await rep?.push();
69 let doc = await publishToPublication({
70 root_entity: props.root_entity,
71 publication_uri: props.publication_uri,
72 leaflet_id: props.leaflet_id,
73 title: props.title,
74 description: props.description,
75 });
76 if (!doc) return;
77
78 let post_url = `https://${props.record?.base_path}/${doc.rkey}`;
79 let [text, facets] = editorStateRef.current
80 ? editorStateToFacetedText(editorStateRef.current)
81 : [];
82 if (shareOption === "bluesky")
83 await publishPostToBsky({
84 facets: facets || [],
85 text: text || "",
86 title: props.title,
87 url: post_url,
88 description: props.description,
89 document_record: doc.record,
90 rkey: doc.rkey,
91 });
92 setIsLoading(false);
93 props.setPublishState({ state: "success", post_url });
94 }
95
96 return (
97 <div className="flex flex-col gap-4 w-[640px] max-w-full sm:px-4 px-3">
98 <h3>Publish Options</h3>
99 <form
100 onSubmit={(e) => {
101 e.preventDefault();
102 submit();
103 }}
104 >
105 <div className="container flex flex-col gap-2 sm:p-3 p-4">
106 <Radio
107 checked={shareOption === "quiet"}
108 onChange={(e) => {
109 if (e.target === e.currentTarget) {
110 setShareOption("quiet");
111 }
112 }}
113 name="share-options"
114 id="share-quietly"
115 value="Share Quietly"
116 >
117 <div className="flex flex-col">
118 <div className="font-bold">Share Quietly</div>
119 <div className="text-sm text-tertiary font-normal">
120 No one will be notified about this post
121 </div>
122 </div>
123 </Radio>
124 <Radio
125 checked={shareOption === "bluesky"}
126 onChange={(e) => {
127 if (e.target === e.currentTarget) {
128 setShareOption("bluesky");
129 }
130 }}
131 name="share-options"
132 id="share-bsky"
133 value="Share on Bluesky"
134 >
135 <div className="flex flex-col">
136 <div className="font-bold">Share on Bluesky</div>
137 <div className="text-sm text-tertiary font-normal">
138 Pub subscribers will be updated via a custom Bluesky feed
139 </div>
140 </div>
141 </Radio>
142
143 <div
144 className={`w-full pl-5 pb-4 ${shareOption !== "bluesky" ? "opacity-50" : ""}`}
145 >
146 <div className="opaque-container p-3 rounded-lg!">
147 <div className="flex gap-2">
148 <img
149 className="rounded-full w-[42px] h-[42px] shrink-0"
150 src={props.profile.avatar}
151 />
152 <div className="flex flex-col w-full">
153 <div className="flex gap-2 pb-1">
154 <p className="font-bold">{props.profile.displayName}</p>
155 <p className="text-tertiary">@{props.profile.handle}</p>
156 </div>
157 <div className="flex flex-col">
158 <BlueskyPostEditorProsemirror
159 editorStateRef={editorStateRef}
160 onCharCountChange={setCharCount}
161 />
162 </div>
163 <div className="opaque-container overflow-hidden flex flex-col mt-4 w-full">
164 {/* <div className="h-[260px] w-full bg-test" /> */}
165 <div className="flex flex-col p-2">
166 <div className="font-bold">{props.title}</div>
167 <div className="text-tertiary">{props.description}</div>
168 <hr className="border-border-light mt-2 mb-1" />
169 <p className="text-xs text-tertiary">
170 {props.record?.base_path}
171 </p>
172 </div>
173 </div>
174 <div className="text-xs text-secondary italic place-self-end pt-2">
175 {charCount}/300
176 </div>
177 </div>
178 </div>
179 </div>
180 </div>
181 <div className="flex justify-between">
182 <Link
183 className="hover:no-underline! font-bold"
184 href={`/${params.leaflet_id}`}
185 >
186 Back
187 </Link>
188 <ButtonPrimary
189 type="submit"
190 className="place-self-end h-[30px]"
191 disabled={charCount > 300}
192 >
193 {isLoading ? <DotLoader /> : "Publish this Post!"}
194 </ButtonPrimary>
195 </div>
196 </div>
197 </form>
198 </div>
199 );
200};
201
202const PublishPostSuccess = (props: {
203 post_url: string;
204 publication_uri: string;
205 record: Props["record"];
206 posts_in_pub: number;
207}) => {
208 let uri = new AtUri(props.publication_uri);
209 return (
210 <div className="container p-4 m-3 sm:m-4 flex flex-col gap-1 justify-center text-center w-fit h-fit mx-auto">
211 <PublishIllustration posts_in_pub={props.posts_in_pub} />
212 <h2 className="pt-2">Published!</h2>
213 <Link
214 className="hover:no-underline! font-bold place-self-center pt-2"
215 href={`/lish/${uri.host}/${encodeURIComponent(props.record?.name || "")}/dashboard`}
216 >
217 <ButtonPrimary>Back to Dashboard</ButtonPrimary>
218 </Link>
219 <a href={props.post_url}>See published post</a>
220 </div>
221 );
222};